package com.tianji.promotion.service.impl;

import cn.hutool.core.bean.copier.CopyOptions;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.common.autoconfigure.mq.RabbitMqHelper;
import com.tianji.common.constants.MqConstants;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.StringUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.discount.Discount;
import com.tianji.promotion.discount.DiscountStrategy;
import com.tianji.promotion.domain.dto.CouponDiscountDTO;
import com.tianji.promotion.domain.dto.OrderCourseDTO;
import com.tianji.promotion.domain.dto.UserCouponDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.domain.po.ExchangeCode;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.enums.ExchangeCodeStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.service.ICouponScopeService;
import com.tianji.promotion.service.IExchangeCodeService;
import com.tianji.promotion.service.IUserCouponService;
import com.tianji.promotion.utils.CodeUtil;
import com.tianji.promotion.utils.MyLock;
import com.tianji.promotion.utils.PermuteUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * <p>
 * 用户领取优惠券的记录，是真正使用的优惠券信息 服务实现类
 * </p>
 *
 * @author zt
 * @since 2024-09-21
 */
@RequiredArgsConstructor
@Service
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {

    private final CouponMapper couponMapper;
    private final IExchangeCodeService exchangeCodeService;
    private final RedisTemplate redisTemplate;
    private final RabbitMqHelper mqHelper;
    private final ICouponScopeService couponScopeService;
    //领取优惠券
    //@Transactional
    @Override
    @MyLock(name = "lock:coupon:uid:#{id}")
    public void receiveCoupon(Long id) {
        //1.校验
        if(id==null){
            throw new BadRequestException("非法参数");
        }
        //Coupon coupon = couponMapper.selectById(id);
        //使用redis获取coupon
        Coupon coupon = queryCouponByCache(id);
        if(coupon==null){
            throw new BadRequestException("优惠券不存在");
        }
        /*if(coupon.getStatus()!= CouponStatus.ISSUING){
            throw new BadRequestException("该优惠券的状态不是发放中");
        }*/
        LocalDateTime now = LocalDateTime.now();
        if(now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())){
            throw new BadRequestException("领取优惠券的时间错误");
        }
        /*if(coupon.getTotalNum()<=0 || coupon.getIssueNum()>=coupon.getTotalNum()){
            throw new BadRequestException("该优惠券库存不足");
        }*/
        if(coupon.getTotalNum()<=0){
            throw new BadRequestException("该优惠券库存不足");
        }
        Long userId = UserContext.getUser();
       /* //使用Redisson实现分布式锁
        String key = "lock:coupon:uid" + userId;
        RLock lock = redissonClient.getLock(key);
        try{
            boolean isLock = lock.tryLock();//看门狗机制会生效 默认失效时间是30秒  看门狗机制不能加过期时间
            if(!isLock){
                throw new BizIllegalException("操作过于频繁");
            }
            //从AOP上下文中获取当前类的代理对象
            IUserCouponService userCouponServiceProxy = (IUserCouponService) AopContext.currentProxy();
            //checkAndCreateUserCoupon(UserContext.getUser(), coupon, null);//这种写法是调用原对象的方法
            userCouponServiceProxy.checkAndCreateUserCoupon(UserContext.getUser(),coupon,null);
            //这种写法调用的是代理对象的方法，方法有事务处理
        }finally {
            lock.unlock();
        }*/
        //统计已领取的数量
        //long num = redisTemplate.opsforHash().get(key,userId);
        String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + id;
        //increment代表本次领取后的 已领数量
        Long increment = redisTemplate.opsForHash().increment(key, userId.toString(), 1);//返回的是自增后的结果
        //校验是否超过限领数量
        //if(num>=coupon.getUserLimit) throw xxx
        if(increment>coupon.getUserLimit()){//由于increment是+1后的结果，所以只能是>不能是>=
            throw new BizIllegalException("超出限领数量");
        }
        //修改优惠券的库存-1
        String couponKey = PromotionConstants.COUPON_CACHE_KEY_PREFIX + id;
        redisTemplate.opsForHash().increment(couponKey,"totalNum",-1);
        //long num = redisTemplate.opsForHash.put(key,userId,num+1);
        //发送消息到mq 消息内容 usrId couponId
        UserCouponDTO userCouponDTO = new UserCouponDTO();
        userCouponDTO.setUserId(userId);
        userCouponDTO.setCouponId(id);
        //发送mq
        mqHelper.send(
                MqConstants.Exchange.PROMOTION_EXCHANGE,
                MqConstants.Key.COUPON_RECEIVE,
                userCouponDTO
        );
    }

    /***
     * 从redis中获取优惠券信息(领取的开始和结束时间、发行的总数量、限领数量)
     * @param id
     * @return
     */
    private Coupon queryCouponByCache(Long id) {
        //1.拼接key
        String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + id;
        //2.在redis中获取优惠券信息
        //获取key的全部信息
        Map entries = redisTemplate.opsForHash().entries(key);
        //将map转换成对象
        Coupon coupon = BeanUtils.mapToBean(entries, Coupon.class, false, CopyOptions.create());
        return coupon;
    }

    /***
     * 兑换码兑换优惠券
     * @param code
     */
    @Override
    @Transactional
    public void exchangeCoupon(String code) {
        //1.校验code是否为空
        if(StringUtils.isBlank(code)){
            throw new BadRequestException("非法参数");
        }
        //2.解析兑换码，得到自增id
        long serialNum = CodeUtil.parseCode(code);//自增id
        //3.判断兑换码是否已兑换 采用redis的bitmap结构 setbit key offset 1 如果方法返回为true代表兑换码已经兑换
        boolean result = exchangeCodeService.updateExchangeCodeMark(serialNum,true);//就是将bitmap中offset的值改为true
        if(result){
            //说明兑换码已经被使用
            throw new BizIllegalException("兑换码已经被使用");
        }
        try {
            //4.判断兑换码是否存在 根据id查询 主键查询
            ExchangeCode exchangeCode = exchangeCodeService.getById(serialNum);
            if (exchangeCode==null){
                throw new BizIllegalException("兑换码不存在");
            }
            //查询用户id
            Long userId = UserContext.getUser();
            //查询优惠券的信息
            Coupon coupon = couponMapper.selectById(exchangeCode.getExchangeTargetId());
            if(coupon==null){
                throw new BadRequestException("优惠券不存在");
            }
            //5.判断是否过期
            LocalDateTime expiredTime = exchangeCode.getExpiredTime();
            LocalDateTime now = LocalDateTime.now();
            if(now.isAfter(expiredTime)){
                throw new BizIllegalException("优惠券已经过期");
            }
            //6.判断是否超出限领数量
            //7.优惠券已发放数量+1
            //8.生成用户券
            //9.更新兑换码状态
            //校验并生成优惠券
            checkAndCreateUserCoupon(userId,coupon,serialNum);
        }catch (Exception e){
            //10.将兑换码的状态重置
            exchangeCodeService.updateExchangeCodeMark(serialNum,false);
        }
    }

    @Transactional
    @Override
    public void checkAndCreateUserCoupon(Long userId, Coupon coupon, Long serialNum) {
        //Long类型的 -128~127在常量池中，是同一个对象，超过该区间是同一个对象
        //Long.toString方法底层是new String，所以还是不同的对象
        //Long.toString().intern() intern方法是强制从常量池中获取字符串，这样就可以保证同一个用户只有同一个对象
        synchronized (userId.toString().intern()) {
            //1.获取当前用户对该优惠券的领取数量
            Integer count = this.lambdaQuery()
                    .eq(UserCoupon::getUserId, UserContext.getUser())
                    .eq(UserCoupon::getCouponId, coupon.getId())
                    .count();
            if (count != null && count >= coupon.getUserLimit()) {
                throw new BadRequestException("以达到领取上限");
            }
            //2.优惠券的已发放数量+1
        /*coupon.setIssueNum(coupon.getIssueNum()+1);
        couponMapper.updateById(coupon);*///因为这个操作不是原子的，会有事务问题，所以不能使用
            couponMapper.incrIssueNum(coupon.getId());//使用乐观锁
            //3.生成用户券
            saveUserCoupon(userId, coupon);
            if (serialNum != null) {
                //4.修改兑换码状态
                boolean success = exchangeCodeService.lambdaUpdate()
                        .set(ExchangeCode::getStatus, ExchangeCodeStatus.USED)
                        .set(ExchangeCode::getUserId, userId)
                        .eq(ExchangeCode::getId, serialNum)
                        .update();
                if (!success) {
                    throw new BadRequestException("更新失败");
                }
            }
        }
    }

    /***
     * 处理mq发过来的消息
     * @param userCouponDTO
     */
    @Override
    @Transactional
    public void checkAndCreateUserCouponNew(UserCouponDTO userCouponDTO) {
        //1.从db中获取优惠券信息
        Coupon coupon = couponMapper.selectById(userCouponDTO.getCouponId());
        if(coupon==null){
            return;
        }
        //2.优惠券的已发放数量+1
        int num = couponMapper.incrIssueNum(coupon.getId());
        if(num==0){
            return;//说明券已经领完
        }
        //3.生成用户券
        saveUserCoupon(userCouponDTO.getUserId(), coupon);
    }

    /***
     * 查询可用的优惠券方案
     * @param orderCourses
     * @return
     */
    @Override
    public List<CouponDiscountDTO> findDiscountSolution(List<OrderCourseDTO> orderCourses) {
        //1.查询当前用户的可用优惠券 coupon和user_coupon表 条件：userId、status=1 查哪些字段:1.优惠券的规则 2.优惠券的id 3.用户id
        Long userId = UserContext.getUser();
        List<Coupon> couponList = this.getBaseMapper().queryMyCopon(userId);
        if(CollUtils.isEmpty(couponList)){
            return CollUtils.emptyList();
        }
        //2.初筛
        //2.1计算订单的总金额 对course的price累加
        int totalAmount = orderCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
        //2.2校验优惠券是否可用
        /*List<Coupon> availiableCoupon = new ArrayList<>();
        for (Coupon coupon : couponList) {
            boolean flag = DiscountStrategy.getDiscount(coupon.getDiscountType()).canUse(totalAmount, coupon);
            if(flag){
                availiableCoupon.add(coupon);
            }
        }*/
        List<Coupon> availableCoupons = couponList.stream().filter(coupon -> DiscountStrategy.getDiscount(coupon.getDiscountType()).canUse(totalAmount, coupon)).collect(Collectors.toList());
        if (CollUtils.isEmpty(availableCoupons)){
            return CollUtils.emptyList();
        }
        //3.细筛(需要考虑优惠券的限定范围)排列组合
        Map<Coupon,List<OrderCourseDTO>> avaMap = findAvailableCoupons(availableCoupons,orderCourses);
        if(CollUtils.isEmpty(avaMap)){
            return CollUtils.emptyList();
        }
        availableCoupons = new ArrayList<>(avaMap.keySet());//这个才是真正可用的优惠券集合
        //优惠方法的排列组合
        List<List<Coupon>> solution = PermuteUtil.permute(availableCoupons);
        for (Coupon availableCoupon : availableCoupons) {
            solution.add(List.of(availableCoupon));
        }
        //4.计算每一种组合的优惠明细

        //5.todo
        return null;
    }

    /***
     * 细筛，查询每一个优惠券对应的可用的课程
     * @param coupons 初筛之后的优惠券集合
     * @param orderCourses 订单中的课程集合
     * @return
     */
    private Map<Coupon, List<OrderCourseDTO>> findAvailableCoupons(List<Coupon> coupons, List<OrderCourseDTO> orderCourses) {
        Map<Coupon,List<OrderCourseDTO>> map = new HashMap<>();
        //1.循环遍历初筛后的优惠券集合
        for (Coupon coupon : coupons) {
            //2.找出每一个优惠券的可用课程
            List<OrderCourseDTO> avaliableCourses = orderCourses;
            //2.1 判断优惠券是否限定了范围 coupon.specific为true
            if(coupon.getSpecific()){
                //2.2 查询限定范围 查询coupon_scope表， 条件coupon_id
                List<CouponScope> scopeList = couponScopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();
                //2.3 得到限定范围的id集合
                List<Long> scopeIds = scopeList.stream().map(CouponScope::getBizId).collect(Collectors.toList());
                //2.4从orderCourses 订单中所有的集合中筛选出该范围内的课程
                avaliableCourses = orderCourses.stream()
                        .filter(orderCourseDTO -> scopeIds.contains(orderCourseDTO.getId()))
                        .collect(Collectors.toList());
            }
            if(CollUtils.isEmpty(avaliableCourses)){
                continue;//说明当前优惠券限定了使用范围，但在订单中的课程中没有找到可用的课程，说明该券不可用，忽略该券，进入下一次循环
            }
            //3.计算该优惠券可用课程的总金额
            int totalAmount = avaliableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
            //4.判断该优惠券是否可用，如果可用添加到map中
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            if(discount.canUse(totalAmount,coupon)){
                map.put(coupon,avaliableCourses);
            }
        }
        return map;
    }

    /***
     * 保存用户表
     * @param userId
     * @param coupon
     */
    private void saveUserCoupon(Long userId, Coupon coupon) {
        UserCoupon userCoupon = new UserCoupon();
        userCoupon.setUserId(userId);
        userCoupon.setCouponId(coupon.getId());
        LocalDateTime termBeginTime = coupon.getTermBeginTime();//优惠券使用的开始时间
        LocalDateTime termEndTime = coupon.getTermEndTime();//优惠券使用的结束时间
        if(termBeginTime==null && termEndTime==null){
            termBeginTime = LocalDateTime.now();
            termEndTime = termBeginTime.plusDays(coupon.getTermDays());
        }
        userCoupon.setTermBeginTime(termBeginTime);
        userCoupon.setTermEndTime(termEndTime);
        this.save(userCoupon);
    }
}
